
//-----------------------------------------------------------------------------
//  
//  Package
//  
//-----------------------------------------------------------------------------

package 
{
	
	//-------------------------------------------------------------------------
	//  
	//  Imports
	//  
	//-------------------------------------------------------------------------
	
	import com.notna.shell.Shell;
	import com.notna.shell.ShellEvent;
	import com.notna.utils.ImmutableArray;
	import com.zm.ZMEngine;
	import com.zm.ZMErrorEvent;
	
	import flash.desktop.NativeApplication;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.InvokeEvent;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.utils.ByteArray;
	import flash.utils.getDefinitionByName;
	import flash.utils.getTimer;
	
	//-------------------------------------------------------------------------
	//  
	//  Class
	//  
	//-------------------------------------------------------------------------
	
	/**
	 * Provides an interactive shell for the ZombieMonkey engine.
	 * 
	 * TO DO:
	 * 	<>Command line options</>
	 * 	Autoloading of specified SWC/SWFs and ES files at startup
	 * 	<done>Clear textfield</done>
	 * 	<unworkable>User control over loaded SWC files via hash code (memory management)</unworkable>
	 * 	<done>Save command history to file</done>
	 * 	<done>Access command history</done>
	 * 	Drag and drop SWC/SWF and ES code to exec in context
	 *  <>Fix history text access (top line only)</>
	 */
	[SWF(backgroundColor="#000000", height="300", width="650")]
	public class ZMShell extends Shell implements ExposedMethods
	{
		
		//---------------------------------------------------------------------
		//  
		//  Fields
		//  
		//---------------------------------------------------------------------
		
		// Shell exposed fields
		public static var _about:Function;
		public static var _clear:Function;
		public static var _eval2:Function;
		public static var _exec:Function;
		public static var _exit:Function;
		public static var _getClassByName:Function;
		public static var _getTimer:Function;
		public static var _history:ImmutableArray;
		public static var _kill:Function;
		public static var _print:Function;
		public static var _reset:Function;
		public static var _save:Function;
		
		// Flash component SWCs (loaded via SWC mechanism at runtime)
		[Embed(source="../swc/flash.swc", mimeType="application/octet-stream")]
		private static const flash:Class;
		[Embed(source="../swc/ik.swc", mimeType="application/octet-stream")]
		private static const ik:Class;
		[Embed(source="../swc/PffLib.swc", mimeType="application/octet-stream")]
		private static const PffLib:Class;
		[Embed(source="../swc/tlfruntime.swc", mimeType="application/octet-stream")]
		private static const tlfruntime:Class;
		[Embed(source="../swc/ui.swc", mimeType="application/octet-stream")]
		private static const ui:Class;
		
		private var _app:NativeApplication;
		private var _engine:ZMEngine;
		private const _engineInfo:String = ZMEngine.BRANCH + "-" + ZMEngine.CHANGE_SET + " (" + ZMEngine.DATE + ")";
		
		//---------------------------------------------------------------------
		//  
		//  Constructor Method
		//  
		//---------------------------------------------------------------------
		
		/**
		 * Constructs a shell for the ZombieMonkey engine.
		 */
		public function ZMShell()
		{
			super(_engineInfo);
			
			// Application setup
			_app = NativeApplication.nativeApplication;
			_app.autoExit = true;
			
			// Stage setup
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.addEventListener(Event.RESIZE, resizeShell, false, 0, true);
			
			// Set up the ES4 engine
			_engine = ZMEngine.engine;
			_engine.addEventListener(Event.COMPLETE, initShell, false, 0, true);
			_engine.addEventListener(ZMErrorEvent.RUNTIME_ERROR, handleES4ErrorEvent, false, 0, true);
			_engine.startup();
		}
		
		//---------------------------------------------------------------------
		//  
		//  Methods
		//  
		//---------------------------------------------------------------------
		
		/**
		 * Prints out a description of the shell application.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function about():String
		{
			var abt:String = "";
			
			var appDesc:XML = _app.applicationDescriptor;
			var ns:Namespace = appDesc.namespace();
			abt += appDesc.ns::name;
			abt += " " + appDesc.ns::versionNumber;
			abt += "\n" + appDesc.ns::copyright;
			
			print(abt);
			
			return abt;
		}
		
		/**
		 * Evaluates the expression as ES4 code.
		 * 
		 * @param expression The code to be evaluated.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function eval2(expression:String):void
		{
			// You need this wrapper method because the compiler runs into
			// tokenizing issues if you directly call internalEval from the
			// shell input
			internalEval(expression);
		}
		
		/**
		 * Retrieves a class definition by its name.
		 * 
		 * @param name The name of the class.
		 * @return The class definition.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function getClassByName(name:String):Class
		{
			return getDefinitionByName(name) as Class;
		}
		
		/**
		 * Returns the time in milliseconds since the application began.
		 * 
		 * @return The time in milliseconds relative to the start of the application.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function getTimer():uint
		{
			return flash.utils.getTimer();
		}
		
		/**
		 * Loads (executes) the given file into the ZMEngine. If the file is a 
		 * text file containing a script, the script is evaluated. If the file 
		 * is a SWF or an SWC, it is loaded as a library.
		 * 
		 * A text file is unloaded (marked for garbage collection) as soon as
		 * it finishes evaluation. SWFs and SWCs must be unloaded manually via
		 * the returned hash code.
		 * 
		 * @param file The (absolute) filepath of the file to be loaded.
		 * @return The hash code used by the ZMEngine to reference the loaded 
		 * data if it is a SWC or SWF.
		 * 
		 * TO DO:
		 * 	Need to give sync option, and a way to check for async
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function exec(filename:String):String
		{
			var result:String;
			
			var f:File = new File(filename);
			
			switch (f.extension)
			{
				case "swc":
				{
					// Need to unzip and pick out the SWF
					var swcBytes:ByteArray = FileUtil.readBytes(filename);
					var swfBytes:ByteArray = new ByteArray();
					
					
					
					// Return hash code
					result = _engine.exec(swfBytes);
				}
					break;
				case "swf":
				{
					// Return hash code
					result = _engine.exec(FileUtil.readBytes(filename));
				}
					break;
				case "es":
				{
					// Don't return a hash code, it is 
					// marked for gc anyways
					eval2(FileUtil.read(filename));
				}
					break;
				default:
				{
					// .as or .js could be handled by .es...but we'll see
				}
					break;
			}
			
			return result;
		}
		
		/**
		 * @private
		 */
		private function internalEval(expression:String):void
		{
			try
			{
				_engine.eval(expression);
			}
			catch (error:Error)
			{
				handleES4Error(error);
			}
		}
		
		/**
		 * Unloads the data in the ZMEngine represented by the hash code.
		 * 
		 * @param hashCode
		 * @return <code>true</code> if the data was marked for garbage collection, 
		 * <code>false</code> otherwise.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function kill(hashCode:String):Boolean
		{
			return _engine.kill(hashCode);
		}
		
		/**
		 * Resets the display, history, and the ZMEngine.
		 * 
		 * EXPOSED TO ZMENGINE
		 */
		public function reset():void
		{
			// Clear output and history
			clear();
			clearHistory();
			
			// Reset the engine
			_engine.reset();
			
			// Reinitialize bindings
			initUtils();
		}
		
		/**
		 * Saves the ZMEngine history to a file.
		 * 
		 * @param filename The path of the file to save the history to.
		 * @param index The index to start concatenating the history. If the index exceeds
		 * the history, an empty file is written.
		 * @param length How many commands to concatenate. If the length index 
		 * is 0, then all history items from the index are concatenated.
		 */
		public function save(filename:String, index:uint = 0, length:uint = 0):void
		{
			var commands:String = "";
			
			var historyLength:int = history["length"];
			
			// Subtract 1 because the save command counts
			if (index > historyLength - 1)
			{
				// Index out of range, empty file
				FileUtil.write(filename, "");
			}
			else
			{
				if (length < 1 || length + index >= historyLength)
				{
					// Ignore the compiler error
					// Okay, I'm OCD, dynamic access, it's dynamic anyways
					// Subtract 1 because the save call gets included and we don't want that
					length = historyLength - index - 1;
				}
				
				for (var i:int = 0; i < length; i++)
				{
					commands += history[i + index] + "\n";
				}
				
				// Save the file
				FileUtil.write(filename, commands);
			}
		}
		
		//---------------------------------------------------------------------
		//  
		//  Event Handlers
		//  
		//---------------------------------------------------------------------
		
		/**
		 * @private
		 */
		private function handleES4Error(error:Error):void
		{
			this.print(error);
		}
		
		/**
		 * @private
		 */
		private function handleES4ErrorEvent(event:ZMErrorEvent):void
		{
			handleES4Error(event.error);
		}
		
		/**
		 * @private
		 */
		private function handleInvokeEvent(event:InvokeEvent):void
		{
			// First argument is always a file
			// Unless the file ending is specifically
			// .swf or .swc
			var args:Array = event.arguments;
			if (args.length > 0)
			{
				// Display what file is being loaded by imitating text input
				var script:String = "exec(\"" + args[0].replace(/\\/g, "\\\\") + "\");";
				
				print(_leadin + script);
				
				// Affects history
				_internalHistory.push(script);
				_historyIndex = _internalHistory.length;
				
				// Actually perform the loading
				exec(args[0]);
			}
		}
		
		/**
		 * @private
		 */
		private function handleShellEventEval(event:ShellEvent):void
		{
			internalEval(event.script);
		}
		
		/**
		 * @private
		 */
		private function handleShellEventExit(event:ShellEvent):void
		{
			// Consider just overriding the shell's exit method
			// instead of using the event listener
			_app.exit();
		}
		
		/**
		 * @private
		 */
		private function initShell(event:Event):void
		{
			// THIS IS ONLY RUN ONCE AT FIRST INIT
			
			if (_engine.isRunning)
			{
				// Trace for debugging
				_engine.eval("trace('" + _engineInfo + "');");
				
				// Reset runtime variables and such
				initUtils();
				
				// Hook up the shell
				init();
				addEventListener(ShellEvent.EXIT, handleShellEventExit, false, 0, true);
				addEventListener(ShellEvent.EVAL, handleShellEventEval, false, 0, true);
				
				// Run invocations
				_app.addEventListener(InvokeEvent.INVOKE, handleInvokeEvent, false, 0, true);
			}
		}
		
		/**
		 * @private
		 */
		private function initUtils():void
		{
			// It is not necessary to load FileUtil since it is already a compiled definition
			_about = about;
			_clear = clear;
			_eval2 = eval2;
			_exec = exec;
			_exit = exit;
			_getClassByName = getClassByName;
			_getTimer = getTimer;
			_history = history;
			_kill = kill;
			_print = print;
			_reset = reset;
			_save = save;
			_engine.eval("" + 
				"function about() { ZMShell._about(); } " + 
				"function clear() { ZMShell._clear(); }" + 
				// Need to avoid conflict with eval(), renamed to eval2()
				"function eval2(expression) { ZMShell._eval2(expression); } " +  
				"function exec(filename) { return ZMShell._exec(filename); } " + 
				"function exit() { ZMShell._exit(); } " + 
				"function getClassByName(name) { return ZMShell._getClassByName(name); } " + 
				"function getTimer() { return ZMShell._getTimer(); } " + 
				"var history = ZMShell._history; " + 
				"function kill(hashCode) { return ZMShell._kill(hashCode); } " + 
				"function print(... expressions) { ZMShell._print.apply(null, (expressions.length == 0 ? [] : expressions)); } " +  
				"function reset() { ZMShell._reset(); } " + 
				"function save(filename, index, length) { ZMShell._save(filename, index, length); }" + 
			"");
		}
		
		/**
		 * @private
		 */
		private function resizeShell(event:Event):void
		{
			resize(stage.stageWidth, stage.stageHeight);
		}
	}
}